package org.jeecg.weixin.cp.tp.service.impl;

import com.google.common.base.Joiner;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.weixin.common.bean.WxAccessToken;
import org.jeecg.weixin.common.bean.WxJsapiSignature;
import org.jeecg.weixin.common.enums.WxType;
import org.jeecg.weixin.common.error.WxCpErrorMsgEnum;
import org.jeecg.weixin.common.error.WxError;
import org.jeecg.weixin.common.error.WxErrorException;
import org.jeecg.weixin.common.error.WxRuntimeException;
import org.jeecg.weixin.common.session.StandardSessionManager;
import org.jeecg.weixin.common.session.WxSessionManager;
import org.jeecg.weixin.common.util.DataUtils;
import org.jeecg.weixin.common.util.RandomUtils;
import org.jeecg.weixin.common.util.crypto.SHA1;
import org.jeecg.weixin.common.util.http.RequestExecutor;
import org.jeecg.weixin.common.util.http.RequestHttp;
import org.jeecg.weixin.common.util.http.SimpleGetRequestExecutor;
import org.jeecg.weixin.common.util.http.SimplePostRequestExecutor;
import org.jeecg.weixin.common.util.json.GsonParser;
import org.jeecg.weixin.cp.bean.*;
import org.jeecg.weixin.cp.config.WxCpTpConfigStorage;
import org.jeecg.weixin.cp.tp.service.*;
import org.apache.commons.lang3.StringUtils;

import java.io.File;
import java.io.IOException;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.Map;

import static org.jeecg.weixin.cp.constant.WxCpApiPathConsts.Tp.*;

/**
 * .
 *
 * @author zhenjun cai
 */
@Slf4j
public abstract class BaseWxCpTpServiceImpl<H, P> implements WxCpTpService, RequestHttp<H, P> {

	private WxCpTpContactService wxCpTpContactService = new WxCpTpContactServiceImpl(this);
	private WxCpTpDepartmentService wxCpTpDepartmentService = new WxCpTpDepartmentServiceImpl(this);
	private WxCpTpMediaService wxCpTpMediaService = new WxCpTpMediaServiceImpl(this);
	private WxCpTpOAService wxCpTpOAService = new WxCpTpOAServiceImpl(this);
	private WxCpTpUserService wxCpTpUserService = new WxCpTpUserServiceImpl(this);

	/**
	 * 全局的是否正在刷新access token的锁.
	 */
	protected final Object globalSuiteAccessTokenRefreshLock = new Object();

	/**
	 * 全局刷新suite ticket的锁
	 */
	protected final Object globalSuiteTicketRefreshLock = new Object();

	/**
	 * 全局的是否正在刷新jsapi_ticket的锁.
	 */
	protected final Object globalJsApiTicketRefreshLock = new Object();

	/**
	 * 全局的是否正在刷新auth_corp_jsapi_ticket的锁.
	 */
	protected final Object globalAuthCorpJsApiTicketRefreshLock = new Object();

	protected final Object globalProviderTokenRefreshLock = new Object();

	protected WxCpTpConfigStorage configStorage;

	private WxSessionManager sessionManager = new StandardSessionManager();

	/**
	 * 临时文件目录.
	 */
	private File tmpDirFile;
	private int retrySleepMillis = 1000;
	private int maxRetryTimes = 5;

	@Override
	public boolean checkSignature(String msgSignature, String timestamp, String nonce, String data) {
		try {
			return SHA1.gen(this.configStorage.getToken(), timestamp, nonce, data).equals(msgSignature);
		} catch (Exception e) {
			log.error("Checking signature failed, and the reason is :" + e.getMessage());
			return false;
		}
	}

	@Override
	public String getSuiteAccessToken() throws WxErrorException {
		return getSuiteAccessToken(false);
	}

	@Override
	public WxAccessToken getSuiteAccessTokenEntity() throws WxErrorException {
		return this.getSuiteAccessTokenEntity(false);
	}

	@Override
	public WxAccessToken getSuiteAccessTokenEntity(boolean forceRefresh) throws WxErrorException {
		getSuiteAccessToken(forceRefresh);
		return this.configStorage.getSuiteAccessTokenEntity();
	}

	@Override
	public String getSuiteTicket() throws WxErrorException {
		if (this.configStorage.isSuiteTicketExpired()) {
			// 本地suite ticket 不存在或者过期
			WxError wxError = WxError.fromJson("{\"errcode\":40085, \"errmsg\":\"invaild suite ticket\"}", WxType.CP);
			throw new WxErrorException(wxError);
		}
		return this.configStorage.getSuiteTicket();
	}

	@Override
	public String getSuiteTicket(boolean forceRefresh) throws WxErrorException {
		// suite ticket由微信服务器推送，不能强制刷新
		// if (forceRefresh) {
		// this.configStorage.expireSuiteTicket();
		// }
		return getSuiteTicket();
	}

	@Override
	public void setSuiteTicket(String suiteTicket) {
		setSuiteTicket(suiteTicket, 28 * 60);
	}

	@Override
	public void setSuiteTicket(String suiteTicket, int expiresInSeconds) {
		synchronized (globalSuiteTicketRefreshLock) {
			this.configStorage.updateSuiteTicket(suiteTicket, expiresInSeconds);
		}
	}

	@Override
	public String getSuiteJsApiTicket(String authCorpId) throws WxErrorException {
		if (this.configStorage.isAuthSuiteJsApiTicketExpired(authCorpId)) {

			String resp = get(configStorage.getApiUrl(GET_SUITE_JSAPI_TICKET),
					"type=agent_config&access_token=" + this.configStorage.getAccessToken(authCorpId));

			JsonObject jsonObject = GsonParser.parse(resp);
			if (jsonObject.get("errcode").getAsInt() == 0) {
				String jsApiTicket = jsonObject.get("ticket").getAsString();
				int expiredInSeconds = jsonObject.get("expires_in").getAsInt();
				synchronized (globalJsApiTicketRefreshLock) {
					configStorage.updateAuthSuiteJsApiTicket(authCorpId, jsApiTicket, expiredInSeconds);
				}
			} else {
				throw new WxErrorException(WxError.fromJson(resp));
			}
		}

		return configStorage.getAuthSuiteJsApiTicket(authCorpId);
	}

	@Override
	public String getSuiteJsApiTicket(String authCorpId, boolean forceRefresh) throws WxErrorException {
		if (forceRefresh) {
			this.configStorage.expireAuthSuiteJsApiTicket(authCorpId);
		}
		return this.getSuiteJsApiTicket(authCorpId);
	}

	@Override
	public String getAuthCorpJsApiTicket(String authCorpId) throws WxErrorException {
		if (this.configStorage.isAuthCorpJsApiTicketExpired(authCorpId)) {

			String resp = get(configStorage.getApiUrl(GET_AUTH_CORP_JSAPI_TICKET),
					"access_token=" + this.configStorage.getAccessToken(authCorpId));

			JsonObject jsonObject = GsonParser.parse(resp);
			if (jsonObject.get("errcode").getAsInt() == 0) {
				String jsApiTicket = jsonObject.get("ticket").getAsString();
				int expiredInSeconds = jsonObject.get("expires_in").getAsInt();

				synchronized (globalAuthCorpJsApiTicketRefreshLock) {
					configStorage.updateAuthCorpJsApiTicket(authCorpId, jsApiTicket, expiredInSeconds);
				}
			} else {
				throw new WxErrorException(WxError.fromJson(resp));
			}
		}
		return configStorage.getAuthCorpJsApiTicket(authCorpId);
	}

	@Override
	public String getAuthCorpJsApiTicket(String authCorpId, boolean forceRefresh) throws WxErrorException {
		if (forceRefresh) {
			this.configStorage.expireAuthCorpJsApiTicket(authCorpId);
		}
		return this.getAuthCorpJsApiTicket(authCorpId);
	}

	@Override
	public WxCpMaJsCode2SessionResult jsCode2Session(String jsCode) throws WxErrorException {
		Map<String, String> params = new HashMap<>(2);
		params.put("js_code", jsCode);
		params.put("grant_type", "authorization_code");

		final String url = configStorage.getApiUrl(JSCODE_TO_SESSION);
		return WxCpMaJsCode2SessionResult
				.fromJson(this.get(url, Joiner.on("&").withKeyValueSeparator("=").join(params)));
	}

	@Override
	public WxAccessToken getCorpToken(String authCorpId, String permanentCode) throws WxErrorException {
		JsonObject jsonObject = new JsonObject();
		jsonObject.addProperty("auth_corpid", authCorpId);
		jsonObject.addProperty("permanent_code", permanentCode);
		String result = post(configStorage.getApiUrl(GET_CORP_TOKEN), jsonObject.toString());

		return WxAccessToken.fromJson(result);
	}

	@Override
	public WxAccessToken getCorpToken(String authCorpId, String permanentCode, boolean forceRefresh)
			throws WxErrorException {
		if (this.configStorage.isAccessTokenExpired(authCorpId) || forceRefresh) {
			WxAccessToken corpToken = this.getCorpToken(authCorpId, permanentCode);
			this.configStorage.updateAccessToken(authCorpId, corpToken.getAccessToken(), corpToken.getExpiresIn());
		}
		return this.configStorage.getAccessTokenEntity(authCorpId);
	}

	@Override
	public WxCpTpCorp getPermanentCode(String authCode) throws WxErrorException {
		JsonObject jsonObject = new JsonObject();
		jsonObject.addProperty("auth_code", authCode);

		String result = post(configStorage.getApiUrl(GET_PERMANENT_CODE), jsonObject.toString());
		jsonObject = GsonParser.parse(result);
		WxCpTpCorp wxCpTpCorp = WxCpTpCorp.fromJson(jsonObject.get("auth_corp_info").getAsJsonObject().toString());
		wxCpTpCorp.setPermanentCode(jsonObject.get("permanent_code").getAsString());
		return wxCpTpCorp;
	}

	@Override
	public WxCpTpPermanentCodeInfo getPermanentCodeInfo(String authCode) throws WxErrorException {
		JsonObject jsonObject = new JsonObject();
		jsonObject.addProperty("auth_code", authCode);
		String result = post(configStorage.getApiUrl(GET_PERMANENT_CODE), jsonObject.toString());
		return WxCpTpPermanentCodeInfo.fromJson(result);
	}

	@Override
	@SneakyThrows
	public String getPreAuthUrl(String redirectUri, String state) throws WxErrorException {
		String result = get(configStorage.getApiUrl(GET_PREAUTH_CODE), null);
		WxCpTpPreauthCode preAuthCode = WxCpTpPreauthCode.fromJson(result);
		String preAuthUrl = "https://open.work.weixin.qq.com/3rdapp/install?suite_id=" + configStorage.getSuiteId()
				+ "&pre_auth_code=" + preAuthCode.getPreAuthCode() + "&redirect_uri="
				+ URLEncoder.encode(redirectUri, "utf-8");
		if (StringUtils.isNotBlank(state)) {
			preAuthUrl += "&state=" + state;
		}
		return preAuthUrl;
	}

	@Override
	@SneakyThrows
	public String getPreAuthUrl(String redirectUri, String state, int authType) throws WxErrorException {
		String result = get(configStorage.getApiUrl(GET_PREAUTH_CODE), null);
		WxCpTpPreauthCode preAuthCode = WxCpTpPreauthCode.fromJson(result);
		String setSessionUrl = "https://qyapi.weixin.qq.com/cgi-bin/service/set_session_info";

		Map<String, Object> sessionInfo = new HashMap<>(1);
		sessionInfo.put("auth_type", authType);
		Map<String, Object> param = new HashMap<>(2);
		param.put("pre_auth_code", preAuthCode.getPreAuthCode());
		param.put("session_info", sessionInfo);
		String postData = new Gson().toJson(param);

		post(setSessionUrl, postData);

		String preAuthUrl = "https://open.work.weixin.qq.com/3rdapp/install?suite_id=" + configStorage.getSuiteId()
				+ "&pre_auth_code=" + preAuthCode.getPreAuthCode() + "&redirect_uri="
				+ URLEncoder.encode(redirectUri, "utf-8");
		if (StringUtils.isNotBlank(state)) {
			preAuthUrl += "&state=" + state;
		}
		return preAuthUrl;
	}

	@Override
	public WxCpTpAuthInfo getAuthInfo(String authCorpId, String permanentCode) throws WxErrorException {
		JsonObject jsonObject = new JsonObject();
		jsonObject.addProperty("auth_corpid", authCorpId);
		jsonObject.addProperty("permanent_code", permanentCode);
		String result = post(configStorage.getApiUrl(GET_AUTH_INFO), jsonObject.toString());
		return WxCpTpAuthInfo.fromJson(result);
	}

	@Override
	public String get(String url, String queryParam) throws WxErrorException {
		return execute(SimpleGetRequestExecutor.create(this), url, queryParam);
	}

	@Override
	public String post(String url, String postData) throws WxErrorException {
		return execute(SimplePostRequestExecutor.create(this), url, postData);
	}

	/**
	 * 向微信端发送请求，在这里执行的策略是当发生access_token过期时才去刷新，然后重新执行请求，而不是全局定时请求.
	 */
	@Override
	public <T, E> T execute(RequestExecutor<T, E> executor, String uri, E data) throws WxErrorException {
		int retryTimes = 0;
		do {
			try {
				return this.executeInternal(executor, uri, data);
			} catch (WxErrorException e) {
				if (retryTimes + 1 > this.maxRetryTimes) {
					log.warn("重试达到最大次数【{}】", this.maxRetryTimes);
					// 最后一次重试失败后，直接抛出异常，不再等待
					throw new WxRuntimeException("微信服务端异常，超出重试次数");
				}

				WxError error = e.getError();
				/*
				 * -1 系统繁忙, 1000ms后重试
				 */
				if (error.getErrorCode() == -1) {
					int sleepMillis = this.retrySleepMillis * (1 << retryTimes);
					try {
						log.debug("微信系统繁忙，{} ms 后重试(第{}次)", sleepMillis, retryTimes + 1);
						Thread.sleep(sleepMillis);
					} catch (InterruptedException e1) {
						Thread.currentThread().interrupt();
					}
				} else {
					throw e;
				}
			}
		} while (retryTimes++ < this.maxRetryTimes);

		log.warn("重试达到最大次数【{}】", this.maxRetryTimes);
		throw new WxRuntimeException("微信服务端异常，超出重试次数");
	}

	protected <T, E> T executeInternal(RequestExecutor<T, E> executor, String uri, E data) throws WxErrorException {
		E dataForLog = DataUtils.handleDataWithSecret(data);

		if (uri.contains("suite_access_token=")) {
			throw new IllegalArgumentException("uri参数中不允许有suite_access_token: " + uri);
		}
		String suiteAccessToken = getSuiteAccessToken(false);

		String uriWithAccessToken = uri + (uri.contains("?") ? "&" : "?") + "suite_access_token=" + suiteAccessToken;

		try {
			T result = executor.execute(uriWithAccessToken, data, WxType.CP);
			log.debug("\n【请求地址】: {}\n【请求参数】：{}\n【响应数据】：{}", uriWithAccessToken, dataForLog, result);
			return result;
		} catch (WxErrorException e) {
			WxError error = e.getError();
			/*
			 * 发生以下情况时尝试刷新suite_access_token 42009 suite_access_token已过期
			 */
			if (error.getErrorCode() == WxCpErrorMsgEnum.CODE_42009.getCode()) {
				// 强制设置wxCpTpConfigStorage它的suite access token过期了，这样在下一次请求里就会刷新suite access
				// token
				this.configStorage.expireSuiteAccessToken();
				if (this.getWxCpTpConfigStorage().autoRefreshToken()) {
					log.warn("即将重新获取新的access_token，错误代码：{}，错误信息：{}", error.getErrorCode(), error.getErrorMsg());
					return this.execute(executor, uri, data);
				}
			}

			if (error.getErrorCode() != 0) {
				log.error("\n【请求地址】: {}\n【请求参数】：{}\n【错误信息】：{}", uriWithAccessToken, dataForLog, error);
				throw new WxErrorException(error, e);
			}
			return null;
		} catch (IOException e) {
			log.error("\n【请求地址】: {}\n【请求参数】：{}\n【异常信息】：{}", uriWithAccessToken, dataForLog, e.getMessage());
			throw new WxRuntimeException(e);
		}
	}

	@Override
	public void setWxCpTpConfigStorage(WxCpTpConfigStorage wxConfigProvider) {
		this.configStorage = wxConfigProvider;
		this.initHttp();
	}

	@Override
	public void setRetrySleepMillis(int retrySleepMillis) {
		this.retrySleepMillis = retrySleepMillis;
	}

	@Override
	public void setMaxRetryTimes(int maxRetryTimes) {
		this.maxRetryTimes = maxRetryTimes;
	}

	public File getTmpDirFile() {
		return this.tmpDirFile;
	}

	public void setTmpDirFile(File tmpDirFile) {
		this.tmpDirFile = tmpDirFile;
	}

	@Override
	public RequestHttp<?, ?> getRequestHttp() {
		return this;
	}

	@Override
	public WxSessionManager getSessionManager() {
		return this.sessionManager;
	}

	@Override
	public WxCpTpUserInfo getUserInfo3rd(String code) throws WxErrorException {
		String url = configStorage.getApiUrl(GET_USERINFO3RD);
		String result = get(url + "?code=" + code, null);
		return WxCpTpUserInfo.fromJson(result);
	}

	@Override
	public WxCpTpUserDetail getUserDetail3rd(String userTicket) throws WxErrorException {
		JsonObject jsonObject = new JsonObject();
		jsonObject.addProperty("user_ticket", userTicket);
		String result = post(configStorage.getApiUrl(GET_USERDETAIL3RD), jsonObject.toString());
		return WxCpTpUserDetail.fromJson(result);
	}

	@Override
	public WxTpLoginInfo getLoginInfo(String authCode) throws WxErrorException {
		JsonObject jsonObject = new JsonObject();
		jsonObject.addProperty("auth_code", authCode);
		jsonObject.addProperty("access_token", configStorage.getProviderToken());
		String responseText = post(configStorage.getApiUrl(GET_LOGIN_INFO), jsonObject.toString());
		return WxTpLoginInfo.fromJson(responseText);
	}

	@Override
	public String getWxCpProviderToken() throws WxErrorException {
		if (this.configStorage.isProviderTokenExpired()) {

			JsonObject jsonObject = new JsonObject();
			jsonObject.addProperty("corpid", configStorage.getCorpId());
			jsonObject.addProperty("provider_secret", configStorage.getProviderSecret());
			WxCpProviderToken wxCpProviderToken = WxCpProviderToken
					.fromJson(this.post(this.configStorage.getApiUrl(GET_PROVIDER_TOKEN), jsonObject.toString()));
			String providerAccessToken = wxCpProviderToken.getProviderAccessToken();
			Integer expiresIn = wxCpProviderToken.getExpiresIn();

			synchronized (globalProviderTokenRefreshLock) {
				configStorage.updateProviderToken(providerAccessToken, expiresIn - 200);
			}
		}
		return configStorage.getProviderToken();
	}

	@Override
	public WxCpProviderToken getWxCpProviderTokenEntity() throws WxErrorException {
		return this.getWxCpProviderTokenEntity(false);
	}

	@Override
	public WxCpProviderToken getWxCpProviderTokenEntity(boolean forceRefresh) throws WxErrorException {
		if (forceRefresh) {
			this.configStorage.expireProviderToken();
		}
		this.getWxCpProviderToken();
		return this.configStorage.getProviderTokenEntity();
	}

	@Override
	public WxCpTpContactService getWxCpTpContactService() {
		return wxCpTpContactService;
	}

	@Override
	public WxCpTpDepartmentService getWxCpTpDepartmentService() {
		return wxCpTpDepartmentService;
	}

	@Override
	public WxCpTpMediaService getWxCpTpMediaService() {
		return wxCpTpMediaService;
	}

	@Override
	public WxCpTpOAService getWxCpTpOAService() {
		return wxCpTpOAService;
	}

	@Override
	public WxCpTpUserService getWxCpTpUserService() {
		return wxCpTpUserService;
	}

	@Override
	public void setWxCpTpContactService(WxCpTpContactService wxCpTpContactService) {
		this.wxCpTpContactService = wxCpTpContactService;
	}

	@Override
	public void setWxCpTpDepartmentService(WxCpTpDepartmentService wxCpTpDepartmentService) {
		this.wxCpTpDepartmentService = wxCpTpDepartmentService;
	}

	@Override
	public void setWxCpTpMediaService(WxCpTpMediaService wxCpTpMediaService) {
		this.wxCpTpMediaService = wxCpTpMediaService;
	}

	@Override
	public void setWxCpTpOAService(WxCpTpOAService wxCpTpOAService) {
		this.wxCpTpOAService = wxCpTpOAService;
	}

	@Override
	public void setWxCpTpUserService(WxCpTpUserService wxCpTpUserService) {
		this.wxCpTpUserService = wxCpTpUserService;
	}

	@Override
	public WxCpTpAdmin getAdminList(String authCorpId, Integer agentId) throws WxErrorException {
		JsonObject jsonObject = new JsonObject();
		jsonObject.addProperty("auth_corpid", authCorpId);
		jsonObject.addProperty("agentid", agentId);
		String result = post(configStorage.getApiUrl(GET_ADMIN_LIST), jsonObject.toString());
		return WxCpTpAdmin.fromJson(result);
	}

	@Override
	public WxJsapiSignature createAuthCorpJsApiTicketSignature(String url, String authCorpId) throws WxErrorException {
		return doCreateWxJsapiSignature(url, authCorpId, this.getAuthCorpJsApiTicket(authCorpId));
	}

	@Override
	public WxJsapiSignature createSuiteJsApiTicketSignature(String url, String authCorpId) throws WxErrorException {
		return doCreateWxJsapiSignature(url, authCorpId, this.getSuiteJsApiTicket(authCorpId));
	}

	private WxJsapiSignature doCreateWxJsapiSignature(String url, String authCorpId, String jsapiTicket) {
		long timestamp = System.currentTimeMillis() / 1000;
		String noncestr = RandomUtils.getRandomStr();
		String signature = SHA1.genWithAmple("jsapi_ticket=" + jsapiTicket, "noncestr=" + noncestr,
				"timestamp=" + timestamp, "url=" + url);
		WxJsapiSignature jsapiSignature = new WxJsapiSignature();
		jsapiSignature.setTimestamp(timestamp);
		jsapiSignature.setNonceStr(noncestr);
		jsapiSignature.setUrl(url);
		jsapiSignature.setSignature(signature);
		jsapiSignature.setAppId(authCorpId);

		return jsapiSignature;
	}

}
